
package com.chasekernan.hxnova.client.plugins;

#if neko

import colhx.TreeNode;
import neko.FileSystem;
import neko.io.File;
import haxe.xml.Check;
import com.chasekernan.hxnova.client.SwhxBase;

/**
    The bare information about a plugin (name, dependencies, and what plugins 
    it's needed by).
**/
typedef BarePlugin = {
    var name            : String;
    var dependencies    : List<String>;
    var neededBy        : List<BarePlugin>;
};

/**
    Manages the loading of plugin files.
**/
class PluginFileManager {
    
    /**
        The directory where the plugin files are stored.
    **/
    public static var PLUGIN_DIRECTORY          = "plugins";
    
    /**
        The file in which the dependency xml information is stored.
    **/
    public static var DEPENDENCIES_FILE         = "dependencies.xml";
    
    /**
        The xml rules for the dependency documents.
    **/
    public static var DEPENDENCY_XML_RULES      = 
            RNode("dependencies", [], 
                    RMulti(RNode("dependency", [Att("name")]), false));
    
    /**
        The name for each element in the dependency file.
    **/
    public static var DEPENDENCY_XML_ELEMENT    = "dependency";
    
    /**
        The extension for plugin files.
    **/
    public static var PLUGIN_EXTENSION          = ".hxnova-plugin";
    
    /**
        Instance of the PluginInstallManager.
    **/
    private static var INSTANCE                 : PluginFileManager;
    
    
    /**
        The trees of plugin dependencies (the lowest children of the first root
        have the highest priority.
    **/
    private var dependencyTree                  : List<TreeNode<String>>;
    
    /**
        The plugins stored as a dictionary by name for easier access.
    **/
    private var pluginDictionary                : Hash<BarePlugin>;
    
    /**
        The list of all the plugins.
    **/
    private var plugins                         : List<BarePlugin>;
    
    
    /**
        Returns the singleton.
    **/
    public static function getInstance() : PluginFileManager {
        if (INSTANCE == null) INSTANCE = new PluginFileManager();
        
        return INSTANCE;
    }
    
    /**
        Returns the path to the given plugin's folder.
    **/
    private static inline function getPath(name : String) : String {
        return PLUGIN_DIRECTORY + "/" + name;
    }
    
    /**
        Returns true if the given list [list] contains [o].
    **/
    private static function contains<T>(list : List<T>, o : T) : Bool {
        for (obj in list) {
            if (obj == o) return true;
        }
        
        return false;
    }
    
    /**
        Private constructor.
    **/
    private function new() {
        dependencyTree      = new List();
        pluginDictionary    = new Hash();
        plugins             = new List();
        
        var instance        = SwhxBase.getInstance();
        instance.addCommunicationObject("PluginFileManager", this);
    }
    
    /**
        Returns the names of all of the installed plugins.
    **/
    public function getInstalledPlugins() : List<String> {
        var plugins = new List<String>();
        
        for (fileName in FileSystem.readDirectory(PLUGIN_DIRECTORY)) {
            switch(FileSystem.kind(getPath(fileName))) {
                case kdir: plugins.add(fileName);
                default: //do nothing
            }
        }
        
        return plugins;
    }
    
    /**
        Returns the names of the plugins on which the given plugin depends.
        
        Returns an empty list if a dependency file isn't found.
        Throws an error if there's a problem loading the file.
    **/
    public function getDependencies(name : String) : List<String> {
        try {
            var deps    = new List<String>();
            var path    = getPath(name) + "/" + DEPENDENCIES_FILE;
            if (!FileSystem.exists(path)) return deps;
            
            var xml     = Xml.parse(File.getContent(path)).firstElement();
            Check.checkNode(xml, DEPENDENCY_XML_RULES);
            
            for (depXml in xml.elementsNamed(DEPENDENCY_XML_ELEMENT)) {
                deps.add(depXml.get("name"));
            }
            
            return deps;
        } catch (e : Dynamic) {
            SwhxBase.getInstance().showMessage("Error", 
                    "Failed to load dependencies file for " + name + ": " + e);
            return new List<String>();
        }
    }
    
    /**
        Returns the order in which the plugins should be loaded.
    **/
    public function getLoadOrder() : List<String> {
        generateDependencyTree();
        
        var order   = new List<String>();
        
        for (root in dependencyTree) {
            TreeNode.visitTree(root, 
                function(node : TreeNode<String>) {
                    order.add(node.data);
                },
            false);
        }
        
        return order;
    }
    
    /**
        Returns the content of the main plugin file given by 
        [name]/[name].[PLUGIN_EXTENSION].
        
        Throws an error if something goes wrong.
    **/
    public function getPluginFile(name : String) : String {
        try {
            var path    = getPath(name) + "/" + name + PLUGIN_EXTENSION;
            return File.getContent(path);
        } catch (e : Dynamic) {
            SwhxBase.getInstance().showMessage("Error",
                    "Error while reading plugin file for " + name + ": " + e);
            return "";
        }
    }
    
    /**
        Generates the depedency tree(s) for the installed plugins.
    **/
    private function generateDependencyTree() {
        plugins         = new List();
        dependencyTree  = new List();
        var pluginNames = getInstalledPlugins();
        
        
        for (name in pluginNames) {
            var plugin  = { name : name, 
                            dependencies : getDependencies(name),
                            neededBy : new List<BarePlugin>() };
            
            plugins.add(plugin);
            pluginDictionary.set(name, plugin);
        }
        
        
        
        for (plugin in plugins) {
            for (dep in plugin.dependencies) {
                var depPlugin           = pluginDictionary.get(dep);
                depPlugin.neededBy.add(plugin);
                checkForDep(depPlugin, plugin);
            }
        }
        
        while (plugins.length != 0) {
            var plugin : BarePlugin     = null;
            
            for (p in plugins) {
                if (p.neededBy.length == 0) {
                    plugin = p;
                    break;
                }
            }
            
            if (plugin == null) throw "Uh oh.";
            
            var tree                    = getDependencyTree(plugin);
            dependencyTree.add(tree);
        }
    }
    
    /**
        Checks to see if there is a dependency loop (ie two plugins require
        each other). If so, an error is thrown.
    **/
    private function checkForDep(dep : BarePlugin, basePlugin : BarePlugin) {
        for (childDep in dep.dependencies) {
            if (childDep == basePlugin.name) {
                SwhxBase.getInstance().showMessage("Error",
                      "Dependency loop found between " + basePlugin.name + 
                      " and " + dep.name + ".");
            }
            
            checkForDep(pluginDictionary.get(childDep), basePlugin);
        }
    }
    
    /**
        Recursively generates the dependency tree for the given plugin.
    **/
    private function getDependencyTree(p : BarePlugin) : TreeNode<String> {
        var node            = new TreeNode<String>(p.name);
        
        for (dep in p.dependencies) {
            var depPlugin   = pluginDictionary.get(dep);
            
            if (contains(plugins, depPlugin)) {
                node.children.add(getDependencyTree(depPlugin));
                plugins.remove(depPlugin);
            }
        }
        
        plugins.remove(p);
        
        return node;
    }
}

#end